<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <h1 data-lake-id="AKxvN" id="AKxvN"><span data-lake-id="u21958035" id="u21958035">典型回答</span></h1>
  <p data-lake-id="u6f7afcd5" id="u6f7afcd5"><span data-lake-id="u2c6b5130" id="u2c6b5130">synchronized其实是一种加锁机制，那么既然是锁，天然就具备以下几个缺点：</span></p>
  <p data-lake-id="u6962c87d" id="u6962c87d"><span data-lake-id="u5a78c088" id="u5a78c088">​</span><br></p>
  <p data-lake-id="ufc722549" id="ufc722549"><span data-lake-id="u21a0e497" id="u21a0e497">1、有性能损耗：虽然在JDK 1.6中对synchronized做了很多优化，如适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁等。但是他毕竟还是一种锁。所以，无论是使用同步方法还是同步代码块，在同步操作之前还是要进行加锁，同步操作之后需要进行解锁，这个加锁、解锁的过程是要有性能损耗的。</span></p>
  <p data-lake-id="u291302e8" id="u291302e8"><span data-lake-id="uec217b4f" id="uec217b4f">​</span><br></p>
  <p data-lake-id="u8733b8e3" id="u8733b8e3"><span data-lake-id="u5dfe6891" id="u5dfe6891">2、产生阻塞：无论是同步方法还是同步代码块，无论是ACC_SYNCHRONIZED还是monitorenter、monitorexit都是基于Monitor实现的。基于Monitor对象，当多个线程同时访问一段同步代码时，首先会进入Entry Set，当有一个线程获取到对象的锁之后，才能进行The Owner区域，其他线程还会继续在Entry Set等待。并且当某个线程调用了wait方法后，会释放锁并进入Wait Set等待。所以，synchronize实现的锁本质上是一种阻塞锁。</span></p>
  <p data-lake-id="u010ad883" id="u010ad883"><span data-lake-id="u05841d37" id="u05841d37">​</span><br></p>
  <p data-lake-id="u371119cd" id="u371119cd"><span data-lake-id="u47fb7c73" id="u47fb7c73">除了前面我们提到的volatile比synchronized性能好以外，volatile其实还有一个很好的附加功能，那就是禁止指令重排。因为volatile借助了内存屏障来帮助其解决可见性和有序性问题，而内存屏障的使用还为其带来了一个禁止指令重排的附加功能，所以在有些场景中是可以避免发生指令重排的问题的。</span></p>
  <p data-lake-id="u63ec663e" id="u63ec663e"><a href="https://www.hollischuang.com/archives/3928" target="_blank" data-lake-id="u51bfc46a" id="u51bfc46a"><span data-lake-id="u73e607b2" id="u73e607b2"><br></span></a></p>
  <h1 data-lake-id="IszIm" id="IszIm"><span data-lake-id="u8ce6c81b" id="u8ce6c81b">扩展知识</span></h1>
  <p data-lake-id="u96e61431" id="u96e61431"><br></p>
  <h2 data-lake-id="On8pc" id="On8pc"><span data-lake-id="u7cf1c5f9" id="u7cf1c5f9">锁的性能损耗</span></h2>
  <p data-lake-id="u32a993c1" id="u32a993c1"><br></p>
  <p data-lake-id="u91aa7671" id="u91aa7671"><span data-lake-id="u590241b8" id="u590241b8">虽然在JDK 1.6中对synchronized做了很多优化，如如适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁等，但是他毕竟还是一种锁。</span></p>
  <p data-lake-id="u7368105e" id="u7368105e"><br></p>
  <p data-lake-id="uf8c879ec" id="uf8c879ec"><span data-lake-id="u85fd2935" id="u85fd2935">以上这几种优化，都是尽量想办法避免对Monitor进行加锁，但是，并不是所有情况都可以优化的，况且就算是经过优化，优化的过程也是有一定的耗时的。</span></p>
  <p data-lake-id="u7dbedebf" id="u7dbedebf"><br></p>
  <p data-lake-id="ue095c9fa" id="ue095c9fa"><span data-lake-id="u48906034" id="u48906034">所以，无论是使用同步方法还是同步代码块，在同步操作之前还是要进行加锁，同步操作之后需要进行解锁，这个加锁、解锁的过程是要有性能损耗的。</span></p>
  <p data-lake-id="ua915375f" id="ua915375f"><br></p>
  <p data-lake-id="u84474a16" id="u84474a16"><span data-lake-id="u952ae8d3" id="u952ae8d3">关于二者的性能对比，由于虚拟机对锁实行的许多消除和优化，使得我们很难量化这两者之间的性能差距，但是我们可以确定的一个基本原则是：volatile变量的读操作的性能和普通变量几乎无差别，但是写操作由于需要插入内存屏障所以会慢一些，即便如此，volatile在大多数场景下也比锁的开销要低。</span></p>
  <p data-lake-id="u7769393a" id="u7769393a"><br></p>
  <h2 data-lake-id="L8QtO" id="L8QtO"><span data-lake-id="uc87b2fa9" id="uc87b2fa9">锁产生阻塞</span></h2>
  <p data-lake-id="u948423a8" id="u948423a8"><br></p>
  <p data-lake-id="u162ccff9" id="u162ccff9"><span data-lake-id="u387c464e" id="u387c464e">无论是同步方法还是同步代码块，无论是ACC_SYNCHRONIZED还是monitorenter、monitorexit都是基于Monitor实现的。</span></p>
  <p data-lake-id="ue0c3d6c0" id="ue0c3d6c0"><br></p>
  <p data-lake-id="u5a2c2f88" id="u5a2c2f88"><span data-lake-id="u1f3a4ebe" id="u1f3a4ebe">基于Monitor对象，当多个线程同时访问一段同步代码时，首先会进入Entry Set，当有一个线程获取到对象的锁之后，才能进行The Owner区域，其他线程还会继续在Entry Set等待。并且当某个线程调用了wait方法后，会释放锁并进入Wait Set等待。</span></p>
  <p data-lake-id="u9b0c04ac" id="u9b0c04ac"><br></p>
  <p data-lake-id="ua321382f" id="ua321382f"><span data-lake-id="ua2c5a483" id="ua2c5a483">所以，synchronize实现的锁本质上是一种阻塞锁，也就是说多个线程要排队访问同一个共享对象。</span></p>
  <p data-lake-id="u2ce3f709" id="u2ce3f709"><br></p>
  <p data-lake-id="uf74b6112" id="uf74b6112"><span data-lake-id="u7abe9149" id="u7abe9149">而volatile是Java虚拟机提供的一种轻量级同步机制，他是基于内存屏障实现的。说到底，他并不是锁，所以他不会有synchronized带来的阻塞和性能损耗的问题。</span></p>
  <p data-lake-id="uc8b32efe" id="uc8b32efe"><br></p>
  <h2 data-lake-id="wyvtu" id="wyvtu"><span data-lake-id="uf5165e94" id="uf5165e94">volatile的附加功能</span></h2>
  <p data-lake-id="u7524b4dd" id="u7524b4dd"><br></p>
  <p data-lake-id="u218ae9a6" id="u218ae9a6"><span data-lake-id="u149a97b9" id="u149a97b9">除了前面我们提到的volatile比synchronized性能好以外，volatile其实还有一个很好的附加功能，那就是禁止指令重排。</span></p>
  <p data-lake-id="u79efe5c1" id="u79efe5c1"><br></p>
  <p data-lake-id="u10407467" id="u10407467"><span data-lake-id="u3eba46c4" id="u3eba46c4">我们先来举一个例子，看一下如果只使用synchronized而不使用volatile会发生什么问题，就拿我们比较熟悉的单例模式来看。</span></p>
  <p data-lake-id="u4f8d14df" id="u4f8d14df"><br></p>
  <p data-lake-id="ubf2e8174" id="ubf2e8174"><span data-lake-id="u498976e8" id="u498976e8">我们通过双重校验锁的方式实现一个单例，这里不使用volatile关键字：</span></p>
  <p data-lake-id="u78af9211" id="u78af9211"><br></p>
  <pre lang="java"><code>
 1   public class Singleton {  
 2      private static Singleton singleton;  
 3       private Singleton (){}  
 4       public static Singleton getSingleton() {  
 5       if (singleton == null) {  
 6           synchronized (Singleton.class) {  
 7               if (singleton == null) {  
 8                   singleton = new Singleton();  
 9               }  
 10           }  
 11       }  
 12       return singleton;  
 13       }  
 14   }
</code></pre>
  <p data-lake-id="u168ba612" id="u168ba612"><br></p>
  <p data-lake-id="ua159ff33" id="ua159ff33"><span data-lake-id="ub7c27857" id="ub7c27857">以上代码，我们通过使用synchronized对Singleton.class进行加锁，可以保证同一时间只有一个线程可以执行到同步代码块中的内容，也就是说singleton = new Singleton()这个操作只会执行一次，这就是实现了一个单例。</span></p>
  <p data-lake-id="ued18a33b" id="ued18a33b"><br></p>
  <p data-lake-id="u585a1443" id="u585a1443"><span data-lake-id="u59b44387" id="u59b44387">但是，当我们在代码中使用上述单例对象的时候有可能发生空指针异常。这是一个比较诡异的情况。</span></p>
  <p data-lake-id="u7f0b5aec" id="u7f0b5aec"><br></p>
  <p data-lake-id="u97080184" id="u97080184"><span data-lake-id="ub756377b" id="ub756377b">我们假设Thread1 和 Thread2两个线程同时请求Singleton.getSingleton方法的时候：</span></p>
  <p data-lake-id="uc5c08805" id="uc5c08805"><br></p>
  <p data-lake-id="u5b6288fb" id="u5b6288fb"><img src="https://cdn.nlark.com/yuque/0/2022/png/5378072/1665897156584-147a00e1-2b52-476e-ba46-e90647459545.png?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_25%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"></p>
  <p data-lake-id="u1a81fa91" id="u1a81fa91"><br></p>
  <p data-lake-id="ud2c0a3c8" id="ud2c0a3c8"><span data-lake-id="u915d0dd3" id="u915d0dd3">Step1 ,Thread1执行到第8行，开始进行对象的初始化。 Step2 ,Thread2执行到第5行，判断singleton == null。 Step3 ,Thread2经过判断发现singleton ！= null，所以执行第12行，返回singleton。 Step4 ,Thread2拿到singleton对象之后，开始执行后续的操作，比如调用singleton.call()。</span></p>
  <p data-lake-id="u457bf2ae" id="u457bf2ae"><br></p>
  <p data-lake-id="u3b341d56" id="u3b341d56"><span data-lake-id="uda4ba981" id="uda4ba981">以上过程，看上去并没有什么问题，但是，其实，在Step4，Thread2在调用singleton.call()的时候，是有可能抛出空指针异常的。</span></p>
  <p data-lake-id="u8a519e71" id="u8a519e71"><br></p>
  <p data-lake-id="u717829f0" id="u717829f0"><span data-lake-id="u1e4b2fe3" id="u1e4b2fe3">之所有会有NPE抛出，是因为在Step3，Thread2拿到的singleton对象并不是一个完整的对象。</span></p>
  <p data-lake-id="u7697cbad" id="u7697cbad"><br></p>
  <p data-lake-id="u6eb7d852" id="u6eb7d852"><span data-lake-id="ud0974d7b" id="ud0974d7b">我们这里来分析一下，singleton = new Singleton();这行代码到底做了什么事情，大致过程如下：</span></p>
  <p data-lake-id="ue342f9d4" id="ue342f9d4"><br></p>
  <ul list="u1a4efb11">
   <li fid="u8c8166dc" data-lake-id="u369a34fd" id="u369a34fd"><span data-lake-id="u756d6c1a" id="u756d6c1a">1、虚拟机遇到new指令，到常量池定位到这个类的符号引用。</span></li>
   <li fid="u8c8166dc" data-lake-id="uf5077b6b" id="uf5077b6b"><span data-lake-id="u490eda56" id="u490eda56">2、检查符号引用代表的类是否被加载、解析、初始化过。 </span></li>
   <li fid="u8c8166dc" data-lake-id="uc730e978" id="uc730e978"><span data-lake-id="udd5188c2" id="udd5188c2">3、虚拟机为对象分配内存。 </span></li>
   <li fid="u8c8166dc" data-lake-id="u6753eaf9" id="u6753eaf9"><span data-lake-id="u4aaa677b" id="u4aaa677b">4、虚拟机将分配到的内存空间都初始化为零值。 </span></li>
   <li fid="u8c8166dc" data-lake-id="u5cd0a026" id="u5cd0a026"><span data-lake-id="u5afc7aac" id="u5afc7aac">5、虚拟机对对象进行必要的设置。</span></li>
   <li fid="u8c8166dc" data-lake-id="u22464b14" id="u22464b14"><span data-lake-id="ud4f9cfc5" id="ud4f9cfc5">6、执行方法，成员变量进行初始化。 </span></li>
   <li fid="u8c8166dc" data-lake-id="ud3f480d3" id="ud3f480d3"><span data-lake-id="u2711b7c2" id="u2711b7c2">7、将对象的引用指向这个内存区域。</span></li>
  </ul>
  <p data-lake-id="uf38a5226" id="uf38a5226"><br></p>
  <p data-lake-id="u843913f4" id="u843913f4"><span data-lake-id="uc67ea422" id="uc67ea422">我们把这个过程简化一下，简化成3个步骤：</span></p>
  <p data-lake-id="ub99ac457" id="ub99ac457"><br></p>
  <ul list="u9b01d64e">
   <li fid="uf3f256bb" data-lake-id="ua52eca5a" id="ua52eca5a"><span data-lake-id="u863b5507" id="u863b5507">a、JVM为对象分配一块内存M </span></li>
   <li fid="uf3f256bb" data-lake-id="u2350294f" id="u2350294f"><span data-lake-id="u8106343b" id="u8106343b">b、在内存M上为对象进行初始化</span></li>
   <li fid="uf3f256bb" data-lake-id="ue3cbd200" id="ue3cbd200"><span data-lake-id="ua44b11dc" id="ua44b11dc">c、将内存M的地址赋值给singleton变量</span></li>
  </ul>
  <p data-lake-id="u478177b0" id="u478177b0"><br></p>
  <p data-lake-id="u514d4a2b" id="u514d4a2b"><img src="https://cdn.nlark.com/yuque/0/2022/png/5378072/1665897242638-f3e87f94-2c09-42c6-a359-1b12eb9cf459.png?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_25%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"></p>
  <p data-lake-id="u063259c9" id="u063259c9"><span data-lake-id="u24325f6e" id="u24325f6e">因为将内存的地址赋值给singleton变量是最后一步，所以Thread1在这一步骤执行之前，Thread2在对singleton==null进行判断一直都是true的，那么他会一直阻塞，直到Thread1将这一步骤执行完。</span></p>
  <p data-lake-id="u78e82625" id="u78e82625"><br></p>
  <p data-lake-id="u3e3e9467" id="u3e3e9467"><span data-lake-id="u733795fd" id="u733795fd">但是，以上过程并不是一个原子操作，并且编译器可能会进行重排序，如果以上步骤被重排成：</span></p>
  <p data-lake-id="ua553dd87" id="ua553dd87"><br></p>
  <ul list="u5bad1e0a">
   <li fid="ub2ed2d1c" data-lake-id="ud16b8885" id="ud16b8885"><span data-lake-id="u2e7403d5" id="u2e7403d5">a、JVM为对象分配一块内存M</span></li>
   <li fid="ub2ed2d1c" data-lake-id="u0b048a8f" id="u0b048a8f"><span data-lake-id="u4e7b30cc" id="u4e7b30cc">c、将内存的地址复制给singleton变量 </span></li>
   <li fid="ub2ed2d1c" data-lake-id="uecfbb7f0" id="uecfbb7f0"><span data-lake-id="u4ee3bb51" id="u4ee3bb51">b、在内存M上为对象进行初始化</span></li>
  </ul>
  <p data-lake-id="u237f1eea" id="u237f1eea"><br></p>
  <p data-lake-id="u9181d12b" id="u9181d12b"><img src="https://cdn.nlark.com/yuque/0/2022/png/5378072/1665897252028-5d852e16-1954-42fd-8fbf-13bd8d39d41e.png?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_25%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"></p>
  <p data-lake-id="u757a99bb" id="u757a99bb"><br></p>
  <p data-lake-id="ue340bab8" id="ue340bab8"><span data-lake-id="ubc49e9c8" id="ubc49e9c8">这样的话，Thread1会先执行内存分配，在执行变量赋值，最后执行对象的初始化，那么，也就是说，在Thread1还没有为对象进行初始化的时候，Thread2进来判断singleton==null就可能提前得到一个false，则会返回一个不完整的sigleton对象，因为他还未完成初始化操作。</span></p>
  <p data-lake-id="u289c0b74" id="u289c0b74"><br></p>
  <p data-lake-id="u56563e01" id="u56563e01"><span data-lake-id="u808ab915" id="u808ab915">这种情况一旦发生，我们拿到了一个不完整的singleton对象，当尝试使用这个对象的时候就极有可能发生NPE异常。</span></p>
  <p data-lake-id="u5a09d5a4" id="u5a09d5a4"><br></p>
  <p data-lake-id="ud08cc9c0" id="ud08cc9c0"><span data-lake-id="u36b8d23c" id="u36b8d23c">那么，怎么解决这个问题呢？因为指令重排导致了这个问题，那就避免指令重排就行了。</span></p>
  <p data-lake-id="u8842ed13" id="u8842ed13"><br></p>
  <p data-lake-id="u155d4ec1" id="u155d4ec1"><span data-lake-id="u57d07e1a" id="u57d07e1a">所以，volatile就派上用场了，因为volatile可以避免指令重排。只要将代码改成以下代码，就可以解决这个问题：</span></p>
  <p data-lake-id="ue3766205" id="ue3766205"><br></p>
  <pre lang="java"><code>
 1   public class Singleton {  
 2      private volatile static Singleton singleton;  
 3       private Singleton (){}  
 4       public static Singleton getSingleton() {  
 5       if (singleton == null) {  
 6           synchronized (Singleton.class) {  
 7               if (singleton == null) {  
 8                   singleton = new Singleton();  
 9               }  
 10           }  
 11       }  
 12       return singleton;  
 13       }  
 14   }
</code></pre>
  <p data-lake-id="u0cff2e12" id="u0cff2e12"><br></p>
  <p data-lake-id="ue2c0730a" id="ue2c0730a"><span data-lake-id="u19bbaa09" id="u19bbaa09">对singleton使用volatile约束，保证他的初始化过程不会被指令重排。</span></p>
 </body>
</html>